package im.composer.audioservers.flac;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javaFlacEncoder.FLACEncoder;
import javaFlacEncoder.StreamConfiguration;

import javax.sound.sampled.AudioFormat;

import org.jaudiolibs.audioservers.AudioClient;
import org.jaudiolibs.audioservers.AudioConfiguration;
import org.kc7bfi.jflac.FLACDecoder;
import org.kc7bfi.jflac.PCMProcessor;
import org.kc7bfi.jflac.metadata.StreamInfo;
import org.kc7bfi.jflac.util.ByteData;
import org.tritonus.share.sampled.FloatSampleTools;

public class FlacAudioClient implements AudioClient, PCMProcessor {

	private final Socket socket;
	private final InputStream in;
	private final OutputStream out;
	private FLACEncoder encoder = new FLACEncoder();
	private final FLACDecoder decoder;
	private AudioConfiguration context;
	private AudioFormat format = null;
	private final List<FloatBuffer> cache = Collections.synchronizedList(new ArrayList<FloatBuffer>());

	public FlacAudioClient(Socket socket) throws IOException {
		super();
		this.socket = socket;
		in = socket.getInputStream();
		out = socket.getOutputStream();
		decoder = new FLACDecoder(in);
		decoder.addPCMProcessor(this);
	}

	@Override
	public void configure(AudioConfiguration context) throws Exception {
		this.context = context;
		if (encoder.samplesAvailableToEncode() > 0) {
			try {
				encoder.encodeSamples(encoder.samplesAvailableToEncode(), true);
			} catch (IOException e) {
			}
			try {
				out.flush();
			} catch (IOException e) {
			}
		}
		encoder = new FLACEncoder();
		int bufz = context.getMaxBufferSize();
		encoder.setOutputStream(new NonSeekableFLACOutputStream(out));
		encoder.setStreamConfiguration(new StreamConfiguration(context.getOutputChannelCount(), bufz, bufz, (int) context.getSampleRate(), 24));
		encoder.openFLACStream();
	}

	@Override
	public void processStreamInfo(StreamInfo streamInfo) {
		format = streamInfo.getAudioFormat();
	}

	@Override
	public void processPCM(ByteData pcm) {
		if (format == null) {
			return;
		}
		byte[] bin = pcm.getData();
		int chan = format.getChannels();
		List<float[]> list = new ArrayList<float[]>(chan);
		for (int i = 0; i < chan; i++) {
			list.add(new float[context.getMaxBufferSize()]);
		}
		FloatSampleTools.byte2float(bin, 0, list, 0, context.getMaxBufferSize(), format);
		synchronized (cache) {
			cache.clear();
			for (float[] arr : list) {
				cache.add(FloatBuffer.wrap(arr));
			}
		}
	}

	@Override
	public boolean process(long time, List<FloatBuffer> inputs, List<FloatBuffer> outputs, int nframes) {
		processInputs(inputs);
		processOutputs(outputs);
		return true;
	}

	private void processInputs(List<FloatBuffer> inputs) {
		float ratio = (float) Math.pow(2, 24);
		int[] arr = new int[context.getInputChannelCount() * context.getMaxBufferSize()];
		int k = 0;
		for (int i = 0; i < context.getMaxBufferSize(); i++) {
			for (int j = 0; j < context.getInputChannelCount(); j++) {
				float sample = inputs.get(j).get(i);
				int _sample = (int) (sample * ratio);
				arr[k] = _sample;
				k++;
			}
		}
		encoder.addSamples(arr, k);
		try {
			encoder.encodeSamples(encoder.fullBlockSamplesAvailableToEncode(), false);
			out.flush();
		} catch (IOException e) {
		}
	}

	private void processOutputs(List<FloatBuffer> outputs) {
		synchronized (cache) {
			int n = Math.min(cache.size(), outputs.size());
			for (int i = 0; i < n; i++) {
				outputs.get(i).put(cache.get(i));
			}
			cache.clear();
		}
	}

	@Override
	public void shutdown() {
		if (encoder != null) {
			try {
				encoder.encodeSamples(encoder.samplesAvailableToEncode(), true);
			} catch (IOException e) {
			}
		}
		if (out != null) {
			try {
				out.flush();
				out.close();
			} catch (Exception e) {
			}
		}
		if (in != null) {
			try {
				in.close();
			} catch (Exception e) {
			}
		}
		try {
			socket.close();
		} catch (Exception e1) {
		}
	}

}
